#include <retro.h>
#include "retro.creator.h"

#define FLAG_TOGGLE(flags, value) \
  if (flags & value) { flags &= ~value; } else { flags |= value; }

using namespace Retro::Creator::GUI;
using namespace SPLib::Geom;

#define CHECKERBOARD_SIZE   16
#define SELECTION_W         2

TileSelector::TileSelector() :
  mFont(_T("Courier New"), 14),
  mBrush(0xFF000000),
  mPen(0xFFFFFFFF)
{
  mSelection = Bounds::get(0, 0, 1, 1);

  scroll.pos.x = scroll.pos.y = 0;
  selectedTile.x = selectedTile.y = -1;

  mMouseDown  = FALSE;
  wBuffer     = NULL;
  mTexture    = NULL;
  erase       = TRUE;
  mTool       = TileToolSelect;

  mFormat.SetAlignment(Gdip::StringAlignmentCenter);
  mFormat.SetLineAlignment(Gdip::StringAlignmentCenter);
}

TileSelector::~TileSelector()
{
  delete wBuffer;
}

VOID  TileSelector::setTool(TileTool iValue)
{
  mTool = iValue;
  invalidate();
}

VOID  TileSelector::onEraseBackground(Event::EraseBackgroundEvent& iEvent)
{
  *iEvent.handled = TRUE;
}

VOID  TileSelector::onPaint()
{
  PAINTSTRUCT wPaint;
  Geom::Bounds wClient = getBounds();
  Gdip::Graphics g(beginPaint(&wPaint));

  if (mTexture)
  {
    Gdip::Rect wUpdate(
    wPaint.rcPaint.left,
    wPaint.rcPaint.top,
    wPaint.rcPaint.right - wPaint.rcPaint.left,
    wPaint.rcPaint.bottom - wPaint.rcPaint.top);

    {
      Gdip::Graphics bg(wBuffer);

      bg.SetClip(Gdip::Rect(
        wUpdate.X + scroll.pos.x,
        wUpdate.Y + scroll.pos.y,
        wUpdate.Width,
        wUpdate.Height));

      bg.Clear(0xFFFFFFFF);

      INT wBoardSize = mTileSize == 1 ? CHECKERBOARD_SIZE : mTileSize / 2;

      Drawing::drawCheckerboard(
        wBoardSize,
        Gdip::Rect(
        (wUpdate.X + scroll.pos.x),
        (wUpdate.Y + scroll.pos.y),
        wUpdate.Width,
        wUpdate.Height),
        bg);

      if (mTexture->getBitmap())
      {
        bg.DrawCachedBitmap(
          &Gdip::CachedBitmap(mTexture->getBitmap(), &g), 0, 0);
      }

      if (!erase && mTool == TileToolSelect)
      {
        Bounds wSelect = mSelection;

        Drawing::snapBounds(wSelect, mTileSize, mTileSize);
        wSelect.absolute();

        bg.DrawRectangle(
          &Gdip::Pen(isFocused() ? 0xFF000000 : 0x7F000000, SELECTION_W),
          wSelect.x + SELECTION_W - 1,
          wSelect.y + SELECTION_W - 1,
          wSelect.cx - (SELECTION_W - 1) * 2,
          wSelect.cy - (SELECTION_W - 1) * 2);
      }

      if (mTexture->getTileset() && mTool != TileToolSelect)
      {
        Geom::Point wTileStart = scroll.pos / (LONG)mTileSize;
        Geom::Point wTileEnd = {
          wTileStart.x + (LONG)ceil((REAL)wClient.cx / mTileSize),
          wTileStart.y + (LONG)ceil((REAL)wClient.cy / mTileSize)
        };

        bg.SetSmoothingMode(Gdip::SmoothingModeHighQuality);

        for (INT x = wTileStart.x; x <= wTileEnd.x; ++x)
        for (INT y = wTileStart.y; y <= wTileEnd.y; ++y)
        {
          drawTile(x, y, bg);
        }
      }
    }

    g.SetClip(wUpdate);
    g.SetCompositingMode(Gdip::CompositingModeSourceCopy);
    g.DrawCachedBitmap(
      &Gdip::CachedBitmap(wBuffer, &g),
      -scroll.pos.x,
      -scroll.pos.y);
    g.ExcludeClip(Gdip::Rect(
      0,
      0,
      mTexture->width,
      mTexture->height));
  }

  g.Clear(0xFFFFFFFF);

  endPaint(&wPaint);
}

VOID  TileSelector::drawTile(LONG x, LONG y, Gdip::Graphics& g)
{
  Data::TileData& wData = mTexture->getTileset()->data[
    x + y * mTexture->width / mTileSize];

  Gdip::RectF wTileBounds(
    x * (REAL)mTileSize,
    y * (REAL)mTileSize,
    (REAL)mTileSize - 1,
    (REAL)mTileSize - 1);

  Gdip::PointF wArrow[POINT_COUNT_ARROW];
  Gdip::RectF wArrowBounds;

  if (x == selectedTile.x && y == selectedTile.y)
  {
    mBrush.SetColor(0x7FFFFFFF);
    g.FillRectangle(&mBrush, wTileBounds);

    mPen.SetColor(0x7F000000);
    mPen.SetWidth(2.0f);
    mPen.SetAlignment(Gdip::PenAlignmentInset);
    g.DrawRectangle(&mPen, wTileBounds);
  }

  switch (mTool)
  {
  case TileToolDirection:
    getArrowBounds(wTileBounds, wArrowBounds, Geom::DirectionLeft);
    Geom::GetLeftArrow(wArrow, wArrowBounds);
    drawArrow(wArrow, wData.flags & TILEDATA_LEFT, g);

    getArrowBounds(wTileBounds, wArrowBounds, Geom::DirectionRight);
    Geom::GetRightArrow(wArrow, wArrowBounds);
    drawArrow(wArrow, wData.flags & TILEDATA_RIGHT, g);

    getArrowBounds(wTileBounds, wArrowBounds, Geom::DirectionUp);
    Geom::GetUpArrow(wArrow, wArrowBounds);
    drawArrow(wArrow, wData.flags & TILEDATA_UP, g);

    getArrowBounds(wTileBounds, wArrowBounds, Geom::DirectionDown);
    Geom::GetDownArrow(wArrow, wArrowBounds);
    drawArrow(wArrow, wData.flags & TILEDATA_DOWN, g);
    break;
  case TileToolFlags:
    _itot(wData.tag, mTileInfo, 10);

    mBrush.SetColor(0xFF000000);
    g.DrawString(mTileInfo, -1, &mFont, wTileBounds, &mFormat, &mBrush);
    break;
  }
}

VOID  TileSelector::drawArrow(
  Gdip::PointF (&iArrow)[POINT_COUNT_ARROW],
  BOOL iSelected,
  Gdip::Graphics& g)
{
  mBrush.SetColor(iSelected ? 0xFF333333 : 0x3F000000);
  g.FillPolygon(&mBrush, iArrow, ARRAYSIZE(iArrow), Gdip::FillModeWinding);
}

VOID  TileSelector::getArrowBounds(
  Gdip::RectF& iSource,
  Gdip::RectF& oBounds,
  Geom::Direction iDirection
  )
{
  switch (iDirection)
  {
  case DirectionUp:
    oBounds = Gdip::RectF(
      iSource.X + iSource.Width / 3,
      iSource.Y,
      iSource.Width / 3,
      iSource.Height / 3);
    break;
  case DirectionLeft:
    oBounds = Gdip::RectF(
      iSource.X,
      iSource.Y + iSource.Height / 3,
      iSource.Width / 3,
      iSource.Height / 3);
    break;
  case DirectionDown:
    oBounds = Gdip::RectF(
      iSource.X + iSource.Width / 3,
      iSource.Y + iSource.Height * 2 / 3,
      iSource.Width / 3,
      iSource.Height / 3);
    break;
  case DirectionRight:
    oBounds = Gdip::RectF(
      iSource.X + iSource.Width * 2 / 3,
      iSource.Y + iSource.Height / 3,
      iSource.Width / 3,
      iSource.Height / 3);
    break;
  }
}

VOID  TileSelector::invalidateTile(LONG x, LONG y)
{
  invalidate(Geom::Rect::get(
    x * mTileSize - scroll.pos.x - 1,
    y * mTileSize - scroll.pos.y - 1,
    x * mTileSize + mTileSize - scroll.pos.x + 1,
    y * mTileSize + mTileSize - scroll.pos.y + 1));
}

VOID  TileSelector::onMouseDown(Event::MouseEvent& iEvent)
{
  if (!mTexture) return;

  Geom::Size wContent = { mTexture->width, mTexture->height };

  focus();
  adjustMousePos(iEvent.point);

  if (iEvent.x < wContent.cx && iEvent.y < wContent.cy)
  {
    if (mTool == TileToolSelect)
    {
      if (mTileSize > 1)
      {
        Geom::Bounds wSelection = iEvent.point + Geom::Size::get(0, 0);

        Drawing::snapBounds(wSelection, mTileSize, mTileSize);
        setSelection(wSelection);
      }
      else
      {
        setSelection(Geom::Bounds::get(0, 0, mTexture->size));
      }

      mDragPoint = iEvent.point;
      mMouseDown = TRUE;
    }
    else if (mTexture->getTileset())
    {
      Geom::Point wTileIndex = {
        iEvent.x / mTileSize,
        iEvent.y / mTileSize
      };

      Gdip::RectF wTileBounds(
        wTileIndex.x * (REAL)mTileSize,
        wTileIndex.y * (REAL)mTileSize,
        (REAL)mTileSize,
        (REAL)mTileSize);

      Data::TileData& wTile = mTexture->getTileset()->data[
        wTileIndex.x + wTileIndex.y * mTexture->width / mTileSize];

      switch (mTool)
      {
      case TileToolDirection:
        {
          Gdip::RectF wArrow;

          sendNotify(TileNotifyEdit);
          invalidateTile(wTileIndex.x, wTileIndex.y);
          Frame::get()->setResourceChanged(mTexture);

          for (INT i = (INT)Geom::DirectionFirst;
            i <= (INT)Geom::DirectionLast; ++i)
          {
            getArrowBounds(wTileBounds, wArrow, (Geom::Direction)i);

            if (wArrow.Contains((REAL)iEvent.x, (REAL)iEvent.y))
            {
              INT wFlag = 0;

              switch ((Geom::Direction)i)
              {
              case DirectionUp:
                wFlag = TILEDATA_UP;
                break;
              case DirectionLeft:
                wFlag = TILEDATA_LEFT;
                break;
              case DirectionDown:
                wFlag = TILEDATA_DOWN;
                break;
              case DirectionRight:
                wFlag = TILEDATA_RIGHT;
                break;
              }

              FLAG_TOGGLE(wTile.flags, wFlag);
              return;
            }
          }

          FLAG_TOGGLE(wTile.flags, TILEDATA_UP);
          FLAG_TOGGLE(wTile.flags, TILEDATA_LEFT);
          FLAG_TOGGLE(wTile.flags, TILEDATA_DOWN);
          FLAG_TOGGLE(wTile.flags, TILEDATA_RIGHT);
          return;
        }
        break;
      case TileToolFlags:
        switch (iEvent.mkButton)
        {
        case MK_LBUTTON:
          wTile.tag += 1;
          break;
        case MK_RBUTTON:
          wTile.tag -= 1;
          break;
        }

        sendNotify(TileNotifyEdit);
        invalidateTile(wTileIndex.x, wTileIndex.y);
        Frame::get()->setResourceChanged(mTexture);
        break;
      }
    }
  }
}

VOID  TileSelector::onMouseUp(Event::MouseEvent& iEvent)
{
  if (!mTexture) return;

  mMouseDown = FALSE;

  mSelection.absolute();

  if (mTileSize > 1)
    Drawing::snapBounds(mSelection, mTileSize, mTileSize);

  setSelection(mSelection);
}

VOID  TileSelector::onMouseMove(Event::MouseEvent& iEvent)
{
  if (!mTexture) return;

  Geom::Rect rect = getClient();

  scrollToPoint(iEvent.point, scroll);
  adjustMousePos(iEvent.point);

  if (mMouseDown)
  {
    mSelection = mDragPoint + Geom::Size::get(iEvent.point - mDragPoint);
    invalidate(FALSE);
  }

  if (mTool != TileToolSelect)
  {
    if (selectedTile.x >= 0 && selectedTile.y >= 0)
      invalidateTile(selectedTile.x, selectedTile.y);

    selectedTile = Geom::Point::get(
      iEvent.x / mTileSize,
      iEvent.y / mTileSize);

    invalidate(Geom::Rect::get(
      selectedTile.x * mTileSize - scroll.pos.x,
      selectedTile.y * mTileSize - scroll.pos.y,
      selectedTile.x * mTileSize + mTileSize - scroll.pos.x,
      selectedTile.y * mTileSize + mTileSize - scroll.pos.y));
  }
}

VOID  TileSelector::onMouseLeave()
{
  if (mTool != TileToolSelect)
  {
    if (selectedTile.x >= 0 && selectedTile.y >= 0)
    {
      invalidateTile(selectedTile.x, selectedTile.y);
      selectedTile.x = selectedTile.y = -1;
    }
  }
}

VOID  TileSelector::onKeyDown(Event::KeyEvent& iEvent)
{
  if (!mTexture) return;

  switch (iEvent.vkCode)
  {
  case VK_PRIOR:
    setScrollY(scroll.pos.y - getClient().getHeight(), scroll);
    return;
  case VK_NEXT:
    setScrollY(scroll.pos.y + getClient().getHeight(), scroll);
    return;
  case VK_HOME:
    setScrollY(0, scroll);
    return;
  case VK_END:
    setScrollY(scroll.max.y, scroll);
    return;
  }

  if (GetKeyState(VK_SHIFT) >> 1)
  {
    if (mTool == TileToolSelect)
    {
      switch (iEvent.vkCode)
      {
      case VK_LEFT:
        mSelection.cx  = max(mTileSize, mSelection.cx - mTileSize);
        setSelection(mSelection);
        scrollToValue(mSelection.getRight() - mTileSize, scroll.pos.y, scroll);
        return;
      case VK_RIGHT:
        mSelection.cx += mTileSize;
        setSelection(mSelection);
        scrollToValue(mSelection.getRight(), scroll.pos.y, scroll);
        return;
      case VK_UP:
        mSelection.cy  = max(mTileSize, mSelection.cy - mTileSize);
        setSelection(mSelection);
        scrollToValue(scroll.pos.x, mSelection.getBottom() - mTileSize, scroll);
        return;
      case VK_DOWN:
        mSelection.cy += mTileSize;
        setSelection(mSelection);
        scrollToValue(scroll.pos.x, mSelection.getBottom(), scroll);
        return;
      }
    }
  }
  else if (GetKeyState(VK_CONTROL) >> 1)
  {
    if (mTool == TileToolSelect)
    {
      switch (iEvent.vkCode)
      {
      case 'A':
        setSelection(Geom::Bounds::get(0, 0, mTexture->size));
        return;
      }
    }
  }
  else
  {
    if (mTool == TileToolSelect)
    {
      switch (iEvent.vkCode)
      {
      case VK_LEFT:
        mSelection.x = max(0, mSelection.x - mTileSize);
        setSelection(mSelection);
        scrollToValue(mSelection.x, scroll.pos.y, scroll);
        return;
      case VK_RIGHT:
        mSelection.x = min(
          mTexture->width - mSelection.cx,
          mSelection.x + mTileSize);
        setSelection(mSelection);
        scrollToValue(mSelection.getRight(), scroll.pos.y, scroll);
        return;
      case VK_UP:
        mSelection.y = max(0, mSelection.y - mTileSize);
        setSelection(mSelection);
        scrollToValue(scroll.pos.x, mSelection.y, scroll);
        return;
      case VK_DOWN:
        mSelection.y = min(
          mTexture->height - mSelection.cy,
          mSelection.y + mTileSize);
        setSelection(mSelection);
        scrollToValue(scroll.pos.x, mSelection.getBottom(), scroll);
        return;
      }
    }
  }
}

VOID  TileSelector::onFocus(Event::FocusEvent& iEvent)
{
  invalidate();
}

VOID  TileSelector::adjustMousePos(Geom::Point& pt)
{
  pt.x = max(pt.x, 0) + scroll.pos.x;
  pt.x = min(pt.x, (INT)mTexture->width - 1);
  pt.y = max(pt.y, 0) + scroll.pos.y;
  pt.y = min(pt.y, (INT)mTexture->height - 1);
}

VOID  TileSelector::setTexture(Data::Texture* iValue)
{
  Geom::Size content = { 0, 0 };

  delete wBuffer;

  if (mTexture = iValue)
  {
    content   = mTexture->size;

    mTileSize = mTexture->getTextureType() == Data::TextureTileset ?
      mTexture->getDataTileset().tileSize : 1;

    wBuffer   = new Gdip::Bitmap(
      mTexture->width,
      mTexture->height,
      PixelFormat32bppARGB);

    switch (iValue->getTextureType())
    {
    case Data::TextureImage:
      setSelection(Geom::Bounds::get(0, 0, mTexture->width, mTexture->height));
      break;
    case Data::TextureTileset:
      setSelection(Bounds::get(0, 0, mTileSize, mTileSize));
      break;
    }
  }
  else
  {
    wBuffer   = NULL;
    mTileSize = 0;
  }

  updateScroll(content, scroll);
}

VOID  TileSelector::setSelection(const Bounds& value)
{
  if (!mTexture) return;

  mSelection.x  = max(0, min(mTexture->width, value.x));
  mSelection.y  = max(0, min(mTexture->height, value.y));
  mSelection.cx = max(0, min(mTexture->width - mSelection.x, value.cx));
  mSelection.cy = max(0, min(mTexture->height - mSelection.y, value.cy));

  erase = FALSE;
  sendNotify(TileNotifySelection);
  invalidate();
}

VOID  TileSelector::setErase(BOOL value)
{
  if (erase = value)
  {
    sendNotify(TileNotifyErase);
  }

  invalidate(FALSE);
}

VOID  TileSelector::onSize(Event::SizeEvent& iEvent)
{
  Geom::Size content = { 0, 0 };

  if (mTexture)
  {
    content.cx = mTexture->width;
    content.cy = mTexture->height;
  }

  updateScroll(content, scroll);
}

VOID  TileSelector::onHScroll(Event::ScrollEvent& iEvent)
{
  setScrollX(calcScrollPos(iEvent.request, SB_HORZ, scroll.pos.x), scroll);
}

VOID  TileSelector::onVScroll(Event::ScrollEvent& iEvent)
{
  setScrollY(calcScrollPos(iEvent.request, SB_VERT, scroll.pos.y), scroll);
}

INT   TileSelector::calcScrollPos(INT sbRequest, INT nBar, INT current)
{
  SCROLLINFO si;

  switch (sbRequest)
  {
  case SB_PAGEUP:
    return current - mTileSize * 5;
  case SB_PAGEDOWN:
    return current + mTileSize * 5;
  case SB_LINEUP:
    return current - mTileSize;
  case SB_LINEDOWN:
    return current + mTileSize;
  case SB_THUMBPOSITION:
  case SB_THUMBTRACK:
    si.cbSize = sizeof(si);
    si.fMask = SIF_TRACKPOS;

    GetScrollInfo(*this, nBar, &si);

    return si.nTrackPos;
  default:
    return current;
  }
}

VOID  TileSelector::onMouseWheel(Event::MouseWheelEvent& iEvent)
{
  if (!mTexture) return;

  if (GetKeyState(VK_SHIFT) >> 1)
  {
    setScrollX(scroll.pos.x - (INT)(
      iEvent.wheelDelta / WHEEL_DELTA * mTileSize), scroll);
  }
  else
  {
    setScrollY(scroll.pos.y - (INT)(
      iEvent.wheelDelta / WHEEL_DELTA * mTileSize), scroll);
  }
}
